package com.lzxr.test.utils;

import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class FileDownloader {
    private static final Log log = LogFactory.get();

    /**
     * 文件分片大小（1M）
     */
    private static final int BLOCK_SIZE = 1024 * 1024;

    /**
     * 临时文件后缀，需要根据实际情况修改
     */
    private static final String TEMP_FILE_SUFFIX = StrPool.DOT + "tmp";

    /**
     * 文件名称
     */
    private static String FILE_NAME;

    /**
     * 文件下载后名称
     */
    private static String FILE_LAST_NAME;

    /**
     * 下载总目录路径
     */
    private static String DOWNLOAD_PATH;

    /**
     * 线程数
     */
    private static final int THREAD_NUM = 10;

    /**
     * 分块下载文件
     *
     * @param url      url
     * @param filename 文件名称
     * @throws Exception 异常
     */
    public static String download(String url, String filename) throws Exception {

        // 文件名赋值
        FILE_NAME = filename;
        FILE_LAST_NAME = filename;
        // 获取文件名（不包含后缀）
        String name = filename.substring(0, filename.lastIndexOf(StrPool.DOT));

        // 获取连接 得到完整文件的长度
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        long fileSize = connection.getContentLengthLong();

        // windows 跟 linux 层级分隔符
        String separator = File.separator;
        // 当前所在目录
        String currentPath = System.getProperty("user.dir");
        // 设置文件下载所在目录 files/xxx
        String filesDirectoryPath = currentPath + separator + "files" + separator;

        // 临时目录名: 文件名
        String tempDirectoryName = name;

        // 临时目录路径
        String tempDirectoryPath = filesDirectoryPath + "temp" + separator + tempDirectoryName + separator;

        // 完整文件保存目录
        DOWNLOAD_PATH = filesDirectoryPath + "download" + separator;

        // 将下载文件的大小和分片数量计算出来
        int splitNum = (int) Math.ceil((double) fileSize / BLOCK_SIZE);

        // 先判断目录是否存在 文件保存目录跟临时文件保存目录
        ArrayList<String> directoryPaths = new ArrayList<>();
        directoryPaths.add(tempDirectoryPath);
        directoryPaths.add(filesDirectoryPath);
        directoryPaths.add(DOWNLOAD_PATH);
        for (String directoryPath : directoryPaths) {
            Path path = Paths.get(directoryPath);
            if (!Files.isDirectory(path)) {
                try {
                    Files.createDirectories(path);
                } catch (IOException e) {
                    e.printStackTrace();
                    return "";
                }
            }
        }

        // 获取配置文件中的线程配置 启动线程下载每个分片
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
        for (int i = 0; i < splitNum; i++) {
            int splitIndex = i;
            String tempFileName = tempDirectoryPath + i + StrPool.UNDERLINE + name + TEMP_FILE_SUFFIX;
            File outFile = new File(tempFileName);
            executor.execute(() -> {
                try {
                    downloadSplit(url, splitIndex, outFile);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }

        // 等待所有分片下载完毕
        executor.shutdown();
        while (!executor.isTerminated()) {
        }

        /* 设定超时时间及单位 */
        executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);

        // 判断分片文件是否全部下载完成
        if (!isAllChunksDownloaded(tempDirectoryPath, splitNum)) {
            return "";
        }

        // 合并文件
        mergeChunks(tempDirectoryPath);

        // 删除临时文件目录以及临时文件
        deleteTempDirectory(tempDirectoryPath);

        log.info("下载任务执行完毕");

        return FILE_LAST_NAME;
    }

    /**
     * 删除临时目录
     *
     * @param tempDirectoryPath 临时目录路径
     * @throws IOException ioexception
     */
    private static void deleteTempDirectory(String tempDirectoryPath) throws IOException {

        Path directory = Paths.get(tempDirectoryPath);
        // 检查目录是否存在
        if (!Files.isDirectory(directory)) {
            return;
        }
        Files.walk(directory)
                .sorted(Comparator.reverseOrder())
                .map(Path::toFile)
                .forEach(File::delete);
    }


    /**
     * 分片下载
     *
     * @param url            url
     * @param splitIndex     第几分片
     * @param temporaryFiles 临时文件
     * @throws IOException ioexception
     */
    private static void downloadSplit(String url, int splitIndex, File temporaryFiles) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setRequestMethod("GET");
        long startByte = (long) splitIndex * BLOCK_SIZE;
        long endByte = (long) (splitIndex + 1) * BLOCK_SIZE - 1;

        // 这里判断进行断点续传
        if (temporaryFiles.exists()) {
            // 从已下载的位置继续下载
            long downloadedBytes = temporaryFiles.length();
            startByte = startByte + downloadedBytes;
            connection.setRequestProperty("Range", "bytes=" + startByte + StrPool.DASHED + endByte);
        } else {
            // 文件不存在说明是第一次下载，不用续传
            connection.setRequestProperty("Range", "bytes=" + startByte + StrPool.DASHED + endByte);
        }

        InputStream in = connection.getInputStream();
        RandomAccessFile out = new RandomAccessFile(temporaryFiles, "rw");
        byte[] buffer = new byte[1024];
        int len;

        if (temporaryFiles.exists()) {
            // 设置断点续传的位置
            out.seek(out.length());
        }
        // 开始写入
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        // 关闭流
        out.close();
        in.close();

        // 关闭此连接
        connection.disconnect();
    }


    /**
     * 将文件分片合并成一个完整的文件
     *
     * @param tempDirectoryPath 分片文件所在的目录
     * @throws IOException ioexception
     */
    public static void mergeChunks(String tempDirectoryPath) throws IOException {
        File chunksDir = new File(tempDirectoryPath);
        // 获取分片文件列表
        List<File> chunkFiles = Arrays.stream(Objects.requireNonNull(chunksDir.listFiles((dir, name) -> name.endsWith(".tmp"))))
                .collect(Collectors.toList());
        // 按文件名升序排序
        chunkFiles = chunkFiles.stream().sorted(Comparator.comparingInt(file -> Integer.parseInt(StrUtil.subBefore(file.getName(), StrPool.UNDERLINE, false))))
                .collect(Collectors.toList());

        // 文件输出路径
        Path filePath = Paths.get(DOWNLOAD_PATH, FILE_NAME);
        // 判断文件是否存在，如果存在就加数字编号
        int index = 0;
        while (Files.exists(filePath)) {
            index++;
            String newName = addNumberSuffix(FILE_NAME, index);
            filePath = Paths.get(DOWNLOAD_PATH, newName);
            FILE_LAST_NAME = newName;
        }

        File mergedFile= filePath.toFile();

        FileOutputStream os = new FileOutputStream(mergedFile, true);
        byte[] buffer = new byte[1024];
        int len;

        for (File file : chunkFiles) {
            InputStream is = Files.newInputStream(file.toPath());
            while ((len = is.read(buffer)) > 0) {
                os.write(buffer, 0, len);
            }
            // 输入流关闭
            is.close();
        }
        // 流关闭
        os.close();
        // 合并完成

        Path absolutePath = filePath.toAbsolutePath();
        log.info("The location where the file is saved : {}", absolutePath);
    }

    /**
     * 判断分片文件是否全部下载完成
     *
     * @param tempDirectoryPath 临时目录路径
     * @param totalChunks       总分片数
     * @return boolean
     */
    public static boolean isAllChunksDownloaded(String tempDirectoryPath, int totalChunks) {
        File chunksDir = new File(tempDirectoryPath);
        File[] files = chunksDir.listFiles(file -> file.getName().endsWith(".tmp"));
        return files != null && files.length == totalChunks;
    }

    /**
     * 添加后缀数量
     * 如第一次下载了 test.txt，下一次再下载此文件，文件保存时文件名为test(1).txt，以此类推
     *
     * @param fileName 文件名称
     * @param index    指数
     * @return {@link String}
     */
    private static String addNumberSuffix(String fileName, int index) {
        // 如果文件名没有后缀，则在文件名后面添加编号
        // 如果有后缀，则在后缀前面添加编号
        int dotIndex = fileName.lastIndexOf(StrPool.DOT);
        StringBuilder sb = new StringBuilder(fileName.length() + 2);
        sb.append(dotIndex >= 0 ? fileName.substring(0, dotIndex) : fileName);
        sb.append("(").append(index).append(")");
        if (dotIndex >= 0) {
            sb.append(fileName.substring(dotIndex));
        }
        return sb.toString();
    }
}
